前言
我们知道在NSObject中有两个初始化的方法,+(void)load
以及+ (void)initialize
方法,这两个方法都是由系统来自动调用,不需要我们手动来调用,我们经常会在这两个方法的内部做一些magic的事情。
下面我们首先对这两个方法做一个初步的使用,看看他们两的调用时机以及异同。
当然如果已经知道了他们的异同以及调用顺序的可以直接看总结或者源码分析
调用时机以及调用策略
我们创建一个xcode工程,然后建立了一个person类,以及一个student类,student类继承于person类,person类里面有一个sayHello的类方法,然后我们创建了一个分类,Student(Extension),我们分别在这三个类下面重写了load
以及initialize
方法,如下所示:
1 | //Persom.m |
然后我们在ViewController的touchBegan
方法里面加入[Studnet hello]
,程序启动之后会打打印出以下信息:
1 | 2018-09-10 16:51:26.487852+0800 Runtime[38325:14188628] person load |
通过上面的打印信息我们可以看出,load方法是由系统自动执行,并且调用的顺序是父类->子类->分类。
接下来我们点击屏幕,打印以下信息:
1 | 2018-09-10 16:53:36.481032+0800 Runtime[38325:14188628] person initialize |
通过打印信息我们知道,只有当该类第一次接收到消息的时候才会调用initialize方法,同样会优先调用父类的initialize方法,但是我们的子类同样也实现了initialize方法,这里只打印了分类的方法,会不会是分类的方法把子类的方法覆盖了?
下面我们来验证一下,我们通过修改将分类的initialize方法注释掉,然后重新运行点击屏幕,打印信息如下:
1 | 2018-09-10 17:00:22.099887+0800 Runtime[39163:14201501] person initialize |
果然验证了我们的猜想,分类的initialize确实会覆盖原类的initialize方法。但是我们还有一个疑问,如果子类也不实现initialize方法的话,这样还会调用到父类的initialize方法吗?
接下来我们将Student类的initialize方法注释掉,看一下打印信息:
1 | 2018-09-10 17:02:47.361138+0800 Runtime[39388:14205240] person initialize |
通过打印信息我们看出来了,如果子类没有实现对应的initialize方法,那么将会调用父类的initialize方法。可是为什么父类initialize方法会被调用两次呢,是不是每个子类第一次收到消息的时候都会先调用父类的initialize方法呢?
接下来我们将ViewController的TouchBegan
方法修改为
1 | [Person hello]; |
我们来看看打印信息:
1 | 2018-09-10 17:06:34.228551+0800 Runtime[39744:14211193] person initialize |
通过打印信息我们看到了当Person收到hello消息的时候,会调用自身的initialize方法,当Person的子类Student收到消息的时候,同样调用了父类的initialize方法(这个是因为之类没有实现initialize方法,刚刚已经论证。),通过和上个打印信息对比,我们发现了如果子类第一次收到消息之前,父类没有收到过消息,也就是没有调用过initialize方法,会先调用initialize初始化父类,如果父类initialize方法已经被调用过,那么就不会调用父类的initialize的方法,而且调用自身的initialize方法,如果自身的initialize方法没有实现,那么就调用父类initialize方法。
我们做了这么多的对比论证,接下来我们来总结一下我们上面得到的结论
总结
通过代码我们可以总结出以下的信息:
- 调用时机
+(void)load
是在类或者分类加入到Objective-C Runtime的时候调用
+(void)initialize
是在类或者子类第一次收到消息的时候调用(类消息或者对象消息)
- 调用顺序
+(void)load
方法的调用顺序是,父类->子类->分类。
+(void)initialize
方法的顺序是,父类->子类(如果有分类,分类方法会替换子类的方法,只执行分类的实现)
- 调用次数
+(void)load
方法只会调用一次
+(void)initialize
有可能会调用多次,如果子类没有实现该方法,则子类第一次收到消息的时候会调用父类的方法。
值得一提的是,如果在子类收到消息之前,父类及其其他子类没有收到过消息,那么会先调用父类的initialize
方法再调用子类的initialize
方法
源码分析
这一系列的对比实验下来我们得出了自己的论证结果,但是从实现原理上面来说我还是对它们的底层实现很感兴趣,接下来我们就来看看源码实现。
源码我这边下载的是 objc4-723版本。
+ (void)load
我们首先来看看在load方法是在哪里调用的以及在load方法之前都做了什么?
1 | //在_objc_init运行时初始化方法里面会注册load_images的回调,当有新的镜像加载到runtime时,都会通知load_images方法 |
- 通过上面的代码我们可以到和load有关的方法有两个,一个是prepare_load_methods以及call_load_methods。下面我们分别来看看这两个方法。
1 | void prepare_load_methods(const headerType *mhdr) |
通过上面的代码我们可以看到,这个方法是为了做一些准备工作。首先会获取镜像里面的所有的Classlist,然后遍历该classList,调用schedule_class_load,传入每一个class。然后全部遍历完成之后会获取category,同样的遍历之后直接加入到loadable_categories列表中。这样是为了保证class的load方法在category的load方法之前调用。
接下来我们来看看schedule_class_load 方法:
1 | static void schedule_class_load(Class cls) |
- 该方法会获取该类的父类然后递归调用
schedule_class_load
方法,保证父类的load方法在子类的load方法之前执行。然后将class 加入到loadable_classes里面。
综上所述,prepare_load_methods这个方法就是将满足load方法的class和category分别存放到loadble_classs以及loadable_categories里面
接下来我们来看看call_load_methods
方法。
1 | void call_load_methods(void) |
- 这一步就是调用loabable_classes 以及 loadable_categories中准备好的load方法,并且class优先于category先调用。接下来我们以call_class_loads为例来看看代码。
1 | static void call_class_loads(void) |
- 从全局变量loadable_classes中得到所有可供调用的class,然后将变量进行清零操作,然后遍历加载所有的class,通过
(*load_method)(cls, SEL_load);
的方式调用+(void)load方法,这里是通过直接调用函数的内存地址的方式来实现调用load方法,而不是objc常见的msg_send()方法。
所以这里就可以解释了为什么子类没有实现load方法的时候不会调用到父类的load方法,因为不是通过msg_send的方式去调用的。也就是每个类的load方法都是独立的,不会有消息的转发等情况发生,也就是利用这个特性,我们可以在这里做method swizzling
+ (void)initialize
接下来我们来看看+ (void)initialize
方法是如何被调用的,我们来关注一下runtime-new.mm文件里面的lookUpImpOrForward方法,我相信有些人看到这个方法应该觉得有点熟悉,这里就是返回一个方法的实现或者消息转发的时候就会调用该方法。
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
- 通过上面的关键代码我们可以看出当一个类的方法任何一个方法被调用的时候,就会判断该类是否已经调用了initialize方法,如果没有调用的话就会通过
void_class_initialize(Class cls)
方法来调用initialize方法,接下来我们来看具体实现。
1 | void _class_initialize(Class cls) |
- 通过上面的代码我们看到了该方法首先会获取该类的父类,并且递归调用
_class_initialize
方法保证父类的方法优先于子类执行。然后我们看到了有一行很关键的代码。
1 | ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); |
- +(void)initialize方法是通过objc_msgSend方法来调用的,区别于load方法通过方法内存地址调用,所以根据objc_msgSend的特性,如果一个子类没有实现initialize方法,那么将会调用父类的initialize方法,如果分类实现了initialize方法,那么将会替换子类方法。
综上所述,一个类的initialize方法是有可能被调用多次的,如果他有对应的子类并且子类没有实现对应的initialize方法的时候本类的initialize方法就会被调用多次。所以为了保证一个类的initialize方法里面的逻辑只会被调用一次,我们可以通过以下的代码判断
1 | + (void)initialize { |